Skip to content

docs(skills): a user field's declared deleteBehavior is taught in the data skill's field-types and relationships rules - #29781

Merged
os-zhuang merged 2 commits into
mainfrom
claude/project-thread-5yjezo
Nov 3, 2026
Merged

os-zhuang merged 2 commits into
mainfrom
claude/project-thread-5yjezo

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

domain:skills#1 · huangyiirene · session_01J8VVAnMUWA9rufXzFaEh4J

Requested by Irene · project thread

Fixes #29400

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:objectstack-data 技能的两份规则文件现在教「user 字段上作者写明的 deleteBehavior 在删除该用户时真的生效」:field-types 的 user 行列出 deleteBehavior(无默认;写了就按 lookup 的方式 cascade / set_null / restrict,没写则保留被删用户的 id);relationships 的 Delete Behaviors 节点名删除联动读 lookup、tree,以及只在声明了值时才读的 user 字段。加一条 eval 用例练它。
  • 为什么改:PR fix(objectql)!: a user field's declared deleteBehavior takes effect when the user is deleted (#29253) #29368 已把该行为合入引擎(card objectql: deleteBehavior on a user field parses but the delete walk never reads it, so restrict / cascade on a user field are not enforced #29253),其 Skills-docs: follow-up 行欠下本卡;技能文本此前只说 user 字段「存储同 lookup」,没说删除用户时会发生什么,AI 写元数据时会把 deleteBehavior 当摆设或漏写。
  • 风险与回滚:纯技能文本与 eval 夹具,无代码、无契约改动。两个 token 天花板都就地付账(删掉在本包别处已有完整版本的 Summary 示例块与 When-to-Use 列表),上限行未动,无 re-wrap。回滚即 revert。
  • 先看:skills/objectstack-data/rules/relationships.md 新段(Delete Behaviors 节末)与被删的 When to Use 节;skills/objectstack-data/rules/field-types.md 的 user 行与被删的 Summary (Roll-up) 块;skills/objectstack-data/evals/objects-fields-relationships.json 的用例 6。
  • 两分钟验证:node scripts/check-skills-token-ratchet.mjs,预期 field-types.md 3183/3207、relationships.md 3844/3873,全绿。
  • 需要你做的:Tier H 受管面(skills/**),需要 GOVERNED_APPROVERS 的批准;PR 保持 draft。

What changes

Facts are taken from current main, not the card text alone: content/docs/data-modeling/fields.mdx as corrected by PR #29712, statesDeleteSemantics / cascadeRelationBehavior in packages/objectql/src/engine.ts, and the pins in packages/objectql/src/engine-cascade-delete-user-field.test.ts.

  1. skills/objectstack-data/rules/field-types.md, the user row. The Key Config cell adds deleteBehavior (no default — declared, deleting the user acts as on lookup: cascade deletes the holders, set_null clears the field or drops that member, restrict refuses with 409 DELETE_RESTRICTED; undeclared, the field keeps the deleted user's id). Everything else in the row is byte-identical.
  2. skills/objectstack-data/rules/relationships.md, Delete Behaviors. One new paragraph after the master_detail code block: the walk also reads lookup and tree (set_null by default — the field is cleared; a multiple field loses only that member — or an authored cascade / restrict), and a user field only when it declares a value: deleting the user then cascades, clears or refuses exactly as a lookup does; undeclared, the field keeps the deleted user's id. restrict, and set_null on a required field (a multiple one only when the removal would empty it), refuse the delete with 409 DELETE_RESTRICTED — the multi-valued narrowing is the engine's (cascadeDeleteRelations escalates unconditionally only for a single-valued field and otherwise refuses only the rows the removal would empty; pinned in engine-cascade-delete.test.ts). The existing master_detail text above it is byte-identical.
  3. skills/objectstack-data/evals/objects-fields-relationships.json. Case 6: a required owner user field that must block deleting a held user (restrict) and a multi-valued watchers field whose deleted member should simply drop out (set_null). Must-contain pins the two deleteBehavior values, type: 'user', multiple: true and required: true; must-not-contain refuses reference: 'user' and deleteBehavior: 'cascade'. The eval README's index line names the user case ("deleteBehavior, on a user field too"). Eval fixtures are measured, not ratcheted.

Token ratchet, paid in place (no ceiling row moved, no re-wrap)

file tokens before / after / ceiling lines before / after
rules/field-types.md 3207 / 3183 / 3207 (headroom 0 → 24) 264 / 250
rules/relationships.md 3869 / 3844 / 3873 (headroom 4 → 29) 420 / 414
evals/objects-fields-relationships.json (not ratcheted) 1436 / 1749 101 / 120
evals/README.md (not ratcheted) 142 / 148 12 / 12
package skills/objectstack-data (17 files) 44130 / 44399 3971 / 3970

The two payments, each a deletion of text restated elsewhere, with its home:

  • field-types.md → the Summary (Roll-up) block under Common Field Configurations (14 lines, 361 bytes). Home: rules/relationships.md → Roll-up Summaries carries the same summaryOperations block with the same keys and comments plus the filter / relationshipField teaching; the Calculated table row in field-types.md keeps the key shape ({ object, field?, function, relationshipField?, filter? }). The deleted block's own last comment pointed at rules/relationships.md.
  • relationships.md → the When to Use lookup vs master_detail section (14 lines) and its link in the table of contents (613 bytes together). Home: the Relationship Types table directly above it (Lifecycle, Required, Use Case columns), the Incorrect vs Correct section (lookup vs master_detail on invoice line items), and the Patterns examples. Its one line not restated there — set_null is the lookup default — moves into the new Delete Behaviors paragraph. Its task.assigned_to → user example taught a lookup to a user object, which this package's own eval 2 forbids (reference: 'user' is must-not-contain; a user object does not exist), so that line was also stale.

The package grows by about 270 tokens: the eval fixture and its index +319 (not loaded into a customer session), the two loaded rule files −24 and −25. Their ceiling rows could be lowered to 3183 and 3844 in a follow-up; this card's file surface does not include scripts/check-skills-token-ratchet.mjs.

Why

PR #29368 (card #29253) made a user field's declared deleteBehavior take effect when the user is deleted, and its Skills-docs: follow-up line owed this card. Before this change the data skill said only that a user field is "stored identically to lookup", and its Delete Behaviors section spoke of master_detail alone, so an AI authoring metadata from the skill had no way to know that deleteBehavior on a user field is now enforced, that it has no default, or that a required field's set_null refuses the delete.

Tests

  • node scripts/check-skills-token-ratchet.mjs: exit 0 — 57 authored bundle file(s) within their ceilings; rules/field-types.md is 3183 tokens (ceiling 3207; headroom 24), rules/relationships.md is 3844 tokens (ceiling 3873; headroom 29).
  • The eval fixture re-serializes byte-identically for cases 1–5 (checked before appending case 6), and every must_contain string of case 6 appears in its expected_output.
  • No code changed, so no package test, typecheck, ablation or dogfood battery is owed (Clause-②: no, no ! in the title).

Acceptance notes

  • The edit is made on the branch the platform fixes for this thread (claude/project-thread-5yjezo) in this container's own checkout rather than in a sibling worktree: the branch can be checked out in one place only and the container is this card's alone. Noted as a deviation from the worktree-first spelling, not from its purpose.
  • master_detail text in both files is left exactly as it was; the card asked for the user teaching only.
  • The lookup row of field-types.md still lists deleteBehavior without its default; relationships.md now states the default once. Not expanded further (small feature, small text).
  • Round 2 (commit 5039eb2) answers the seat's contract review of dceca37 (comment 6595584700): M1, the required + set_null sentence now carries the multi-valued qualifier; S1, the eval README index line reads as a sentence. Nothing else moved. content/docs/data-modeling/fields.mdx carries the same unqualified sentence for a user field and is the docs lane's follow-up, not this PR's.

Verification

Head 5039eb23 (two commits: dceca37 the change, 5039eb2 the review's M1 + S1). No code changed, so no package test or typecheck is owed; the derived gate set is the verification.

Local gates on 5039eb2: 32 derived, 31 run with exit 0, 1 NOT MEASURED (`dispatch-gates --ran`); the same 32 on dceca37 read the same
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 32 commands for the four changed files, the same list on both heads. All 32 ran on each head in a tree with pnpm install --frozen-lockfile done and @objectstack/formula, @objectstack/lint, @objectstack/spec and @objectstack/objectql built, each with a 900 s timeout, each exit code captured before any pipe.
  • 31 exited 0 on 5039eb2, among them check-skills-token-ratchet.mjs (and its --self-test), check-comment-mask-corpus.mjs, check:doc-authoring, check:pm-skill-id-lint, check:skill-compatibility, check:skill-frame-sync, check:skill-identifier-liveness, check:role-word, check:nul-bytes, check:pm-governed-merges, check:dts-closure and check:lean-entry-closure (on dceca37 the last exited 3 before the @objectstack/objectql build and 0 after it).
  • pnpm check:dual-build-cjs-loads exited 3: PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/ (91 packages; it needs a full pnpm build). Not a finding: this diff touches no packages/** file, and the gate's own text says nothing was measured. CI's Lint & Repo Gates run on the PR is its reading.
  • --ran printed: 32 derived famil(ies) accounted for — 31 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3).
  • node scripts/check-commit-card-trailers.mjs --range origin/main..HEAD: exit 0 on both heads (two commits, no card relation, model-free trailer pair).
  • CI on dceca37: every check-run success or skipped by path filter or opt-in, including the required Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core, Governed Surface Queue Guard, Test UI and Live E2E UI. CI on 5039eb2: in progress at the time of writing.
  • Writes: git push ×3 (the bare branch pointer, dceca37, 5039eb2); one relay pr_create carrying skip-changeset; no GitHub MCP write.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (No schema touched.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint is CI's repo-wide run.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. Nothing publishes from a released package: skip-changeset rides the create; both declaration lines are above.
  • 6. packages/spec not touched.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the 维护者速读 block is the repo's own Chinese reading aid).

🤖 Generated with Claude Code

https://claude.ai/code/session_01J8VVAkZCePk1V8qh465qcT


Generated by Claude Code

…eteBehavior takes effect when the user is deleted

The `user` row of the field-types rule lists `deleteBehavior` with the walk's
behaviour (no default; declared, it acts as on a lookup; undeclared, the field
keeps the deleted user's id), and the delete-behaviours section of the
relationships rule names `lookup`, `tree` and a declaring `user` field among
the reference types the walk reads. One eval case exercises it.

Each ceiling is paid in place: the field-types Summary (Roll-up) block
(restated by the relationships rule's Roll-up Summaries) and the relationships
"When to Use lookup vs master_detail" bullets (restated by the Relationship
Types table and the Incorrect vs Correct section; its lookup-to-user example
contradicted the package's own eval) are deleted.

Claude-Session: https://claude.ai/code/session_01J8VVAkZCePk1V8qh465qcT
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Nov 3, 2026
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation labels Nov 3, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · huangyiirene · session_01J8VVAnMUWA9rufXzFaEh4J

Contract-review-claim: session_01J8VVAnMUWA9rufXzFaEh4J · head dceca37 · 2026-11-03T04:25Z

The seat review runs at CONTRACT_REVIEW_TIER in an isolated subagent (the seat session is not at that tier).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · huangyiirene · session_01J8VVAnMUWA9rufXzFaEh4J

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: dceca3722feb53f101c545343928a1f6b1828faf
Local-runs: probe — on a detached worktree of the head (merge-base with origin/main 6f09464; origin/main at 2c91a55 when fetched; none of skills/objectstack-data/**, content/docs/data-modeling/fields.mdx, packages/objectql/src/engine.ts, packages/objectql/src/engine-cascade-delete-user-field.test.ts, packages/spec/src/data/field.zod.ts or scripts/check-skills-token-ratchet.mjs moved on main since the merge-base): node scripts/check-skills-token-ratchet.mjs on the head (exit 0: field-types.md 3183 / 3207, relationships.md 3830 / 3873, 57 authored files within ceiling) and on a second detached worktree at the merge-base (exit 0: 3207 / 3207, 3869 / 3873) plus its --self-test (exit 0, 97 cases); node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (32 commands, matching the dev record); node scripts/pm/check-skill-id-lint.mjs (self-test 23 cases, 109 files clean, exit 0); node scripts/check-skill-identifier-liveness.mjs (self-test OK; 486 citations over 71 published files, exit 0); check-skill-compatibility-version.mjs, check-skill-frame-sync.mjs, check-role-word.mjs (self-test and live, all exit 0); node scripts/pm/check-governed-merges.mjs --branch HEAD (exit 3, GOVERNED, Tier H, 4 of 4 paths hit skills/**, 60 changed lines); node scripts/check-commit-card-trailers.mjs --range origin/main..HEAD and --range 6f094645..HEAD (exit 0, one commit, model-free trailer pair); check-doc-authoring.mjs and check-dual-build-cjs-loads.mjs (both exit 3 PREREQUISITE NOT MET in this container: no typescript in node_modules, no dist/; nothing measured, read from CI); node -e require(...) over the eval fixture on head and merge-base (parses; cases 1–5 byte-identical as JSON; every case-6 must_contain string in its expected_output); diff of the two rule files with the touched lines removed (byte-identical remainder, no re-wrap); byte and line counts of the two deleted blocks at the merge-base; gh api REST reads of the PR, its files, commits, comments and reviews, card #29400 and its comments, and the head's check-runs three times. No check:skill-docs alias exists in the head's package.json, so none was run. No evals runner exists in the repo (the ratchet classes the file as an eval fixture; must_contain has no local executor), so case 6 is judged on parse, internal coherence and agreement with the code. Both worktrees were removed after the runs and the main checkout was not touched.
Scope: skills/objectstack-data/rules/field-types.md (the governed hit: the user row L96; the deleted Summary (Roll-up) block, merge-base L206–219), skills/objectstack-data/rules/relationships.md (the governed hit: ToC L3, the deleted When to Use section, merge-base L19–32, the new Delete Behaviors paragraph L175–180), skills/objectstack-data/evals/objects-fields-relationships.json (case 6), skills/objectstack-data/evals/README.md (the index line) + read, not reviewed: packages/objectql/src/engine.ts on the head (DELETE_SEMANTICS_REFERENCE_TYPES L1818, USER_REFERENCE_TYPE / DECLARED_DELETE_BEHAVIORS L1847–1848, statesDeleteSemantics L1859–1865, cascadeRelationBehavior L19645–19715, the required + set_null escalation L20073–20076 and its multi-valued deferral L20167–20175), packages/objectql/src/engine-cascade-delete-user-field.test.ts (every pin), packages/objectql/src/engine-cascade-delete.test.ts L334 and L416–459 (the multi-valued required set_null narrowing), packages/spec/src/data/field.zod.ts L1840–1862 and L3325–3341 (deleteBehavior enum and the lookup / tree default materialization), content/docs/data-modeling/fields.mdx L290–310 on main, skills/objectstack-data/SKILL.md L40–48 and L282–283 (the routing index and the user sentence), skills/objectstack-data/references/lint-rules.md L14 and L51, scripts/check-skills-token-ratchet.mjs, card #29400 body, comments 6592686110 (triage) and 6595137045 (claim), PR #29781 body and its one comment 6595428260 (the seat's review claim on this head), the dev gate record pr-29400/gates-dceca372.txt (read as input)

Reviewer: an isolated contract reviewer at CONTRACT_REVIEW_TIER, read-only on GitHub (REST reads only; nothing written, pushed or commented), fed the card body and comments, the PR body, the net diff against the merge-base, the head's check-runs, AGENTS.md, the pm-dispatch SKILL.md contract-review section, contract-review.md, review-checklist.md and lanes/skills.md. The dev gate record was read as input, not as conclusions; every count and every claim about the engine below is this review's own reading of the head tree. No environment variable value is printed anywhere in this record.

① Derived judgments

  1. File surface. One commit (dceca37, author Claude, PR author objectstack-fleet[bot], head repo = base repo, draft, base main, trailers Claude-Session: + Co-authored-by: Claude, no model identifier). git diff --numstat 6f094645 dceca372, the REST file listing (+29 / −31, 4 files) and the PR body agree on exactly four paths: rules/field-types.md +1/−15 (264 → 250 lines), rules/relationships.md +8/−15 (420 → 413), evals/objects-fields-relationships.json +19/−0 (101 → 120), evals/README.md +1/−1 (12 lines both). The claim (6595137045) named the two rule files, "plus their evals where the edited lines are covered"; the fixture is that, and the README is the fixture's one-line index, so it rides with it. Nothing outside skills/objectstack-data/ is touched; the ratchet script is not in the diff. Correct, and on surface.
  2. The code the new text asserts, read on the head. statesDeleteSemantics (engine.ts L1859–1865) admits master_detail, lookup and tree unconditionally and a user field only when deleteBehavior is one of set_null / cascade / restrict (L1848). cascadeRelationBehavior (L19713–19714) resolves a non-master_detail field to fdef.deleteBehavior || 'set_null', and the docblock at L19705–19707 says the fallback is never what a user gets. field.zod.ts L3339–3340 materializes set_null on lookup and tree only; a bare user field parses with no key (pinned by the test's first case, L197–201). The user-field test pins: undeclared → holder kept with the id unchanged (L203–218, four shapes including required: true); restrict → 409 DELETE_RESTRICTED with the same code and status a lookup restrict answers (L221–255); cascade → the rows holding the user are deleted, a bystander stays, and a later restrict rolls the cascade back as one unit of work (L281–316); set_null single → field null, multi → only the member removed and an emptied set is [] (L319–346); set_null on a required single-valued field → refused (L348–359). So the field-types user row ("no default — declared, deleting the user acts as on lookup: cascade deletes the holders, set_null clears the field or drops that member, restrict refuses with 409 DELETE_RESTRICTED; undeclared, the field keeps the deleted user's id") states exactly what the code does, and the relationships paragraph's first two sentences ("The walk also reads lookup and tree (set_null by default … or an authored cascade / restrict), and a user field only when it declares a value … undeclared, the field keeps the deleted user's id") do too. The three values that exist, what each does, the join-only-when-declared rule and the 409 code are all correct against the code.
  3. The required + set_null sentence, relationships.md L179–180: "restrict, and set_null on a required field, refuse the delete with 409 DELETE_RESTRICTED." The code escalates unconditionally only for a single-valued field (L20073–20076, requiredSetNull && !multiValued). For a multiple: true required field the decision is deferred to after the probe (L20167–20175) and refuses only when some row's set would be EMPTIED by the member removal; a row that still holds another live member is written the non-empty remainder and the delete goes through. That is a maintainer ruling (cascadeDeleteRelations refuses a required multi-value lookup delete even when member removal would leave the set non-empty #9688, 2026-08-19), pinned at engine-cascade-delete.test.ts L334 ("a required MULTI-VALUE lookup now REMOVES the member when the remainder stays non-empty") and L416–459, and the engine's own comment calls the unconditional reading "broader than the contract". The skill's sentence is the pre-ruling reading, and it reaches user fields through the paragraph's own "exactly as a lookup does". A watchers-shaped field (multiple: true, required: true, deleteBehavior: 'set_null') is the natural case the paragraph is teaching, and the skill tells its author that deleting any watcher is refused when the engine refuses only the last one. The code contradicts the statement for that case: M1. The fix is a parenthetical; relationships.md has 43 tokens of headroom. fields.mdx L309 on main carries the same simplification for the user field, so the docs page is not a defence of the sentence and owes its own follow-up (③). Eval 6 is untouched by this: its owner is single-valued and its watchers is not required.
  4. Cut 1, field-types.md merge-base L206–219 (the Summary (Roll-up) heading and code block: type: 'summary', summaryOperations with object / field / function, the function list 'count' | 'sum' | 'min' | 'max' | 'avg', and the comment "relationshipField / filter — optional (see rules/relationships.md)"). Line by line against relationships.md L188–203 on the head: type: 'summary' (L191), summaryOperations (L192), object with "child object to aggregate" (L193), field with "child field to aggregate (ignored for count)" (L194, a superset), function with the identical list (L195), relationshipField and filter as full comments (L196–200) instead of a pointer. The field-types Calculated row L132 keeps the key shape { object, field? (omit for count), function, relationshipField?, filter? }. The only difference is the example child name (invoice_line_item vs invoice_line), not a fact. Nothing taught only there. Survives. Measured 14 lines, 361 bytes, as the body says.
  5. Cut 2, relationships.md merge-base L19–32 (the When to Use section) and the ToC link on L3. Line by line: "Child record can exist independently" → Relationship Types table L15 Lifecycle "Independent" and the lookup pattern's required: false, // Contact can exist without account (L40); "Parent deletion should only clear the FK (deleteBehavior: 'set_null', the default)" → the new paragraph L175–176 ("set_null by default — the field is cleared"), the body's own named exception; "Relationship is optional" → L15 "Optional by default"; "Example: task.assigned_to → user" → see ①.6; "Child record is meaningless without parent" → L16 "Coupled (cascade delete)" / "Owned by" and Incorrect vs Correct L364 and L383 ("Child owned by parent"); "Parent deletion should cascade to children" → L16 and the Delete Behaviors table L164; "Relationship is mandatory" → L16 Required "Forced only under controlled_by_parent; else lint-warned" (more precise than the cut, which overstated it) and the Correct block's required: true L386; "Example: invoice_line_item.invoice_id → invoice" → the Incorrect vs Correct pair uses exactly invoice_line_item.invoice_id → invoice (L366–389). No fact survives only in the cut. The anchor #when-to-use-lookup-vs-master_detail has no remaining reader in skills/, .claude/ or content/docs (grep); SKILL.md L42 still indexes relationships.md as "lookup vs master_detail, junction patterns, delete behaviors", which the table and the Incorrect vs Correct section still deliver. Survives. Measured 14 lines, 534 bytes, plus 79 bytes of ToC (123 → 44) = 613, as the body says.
  6. The task.assigned_to → user claim. The bullet sat under "Use lookup When", so it taught a lookup whose reference is user. Eval 2's expected_output ends "added_by is type: 'user' — never a lookup to a user object, which does not exist" and its must_not_contain carries reference: 'user'; eval 6 repeats both. The engine fixes a user field's target to sys_user (engine.ts L1829–1830, test L269–278), and the field-types row says the same. The deleted line contradicted the package's own eval and the code; the body's claim holds, and the lost example's content ("the child can exist without the reference") has the contact → account example as its home. Correct.
  7. Token ratchet. Measured on both trees: field-types.md 3207 → 3183 (ceiling 3207, headroom 0 → 24), relationships.md 3869 → 3830 (ceiling 3873, headroom 4 → 43); both exit 0, the self-test passes (97 cases), and the ratchet script is not in the diff so no ceiling row moved. The eval fixture 1436 → 1749 and README 142 → 149 are measured, not ratcheted. Per-file rows for the 17-file package sum to 44131 → 44388 (+257); the body says 44130 → 44386 (+256), a one-to-two-token difference in how SKILL.md's unpriced stamp line is counted, immaterial. No re-wrap: with the touched lines removed (field-types L96 and merge-base L206–219; relationships L3, merge-base L19–32 and head L175–181) the remainders of both files are byte-identical to the merge-base, and the word-diff shows 36 changed tokens, every one inside those lines. Paid line by line. Correct.
  8. Evals. The fixture parses on both trees; cases 1–5 re-serialize to identical JSON; case 6 is appended with id: 6, files: [], five must_contain strings (type: 'user', deleteBehavior: 'restrict', deleteBehavior: 'set_null', multiple: true, required: true) each present in its expected_output, and two must_not_contain strings (reference: 'user', deleteBehavior: 'cascade') each absent from it. Its facts agree with the code: owner required + restrict → 409 DELETE_RESTRICTED (test L221–237); "set_null on a required field would refuse too" is said of owner, single-valued, so it is correct (test L348–359); watchers multiple: true + set_null → only the member removed, emptied set [] (test L333–346); "a user field with no deleteBehavior keeps the deleted user's id" (test L203–218). README index line gains "a user field's included" (142 → 149 tokens). Not touched by this PR but seen: eval 1's must_not_contain type: 'password' and eval 2's must_not_contain many_to_many each appear in their own expected_output (the expected text names what to avoid) — fixtures an ideal answer written as the expected output would fail; pre-existing, follow-up (③). Case 6 does not repeat that defect. Correct.
  9. Gates re-run here. The 32 derived commands match the dev record's 32. Of those re-run: token ratchet and self-test, skill-id-lint, skill-identifier-liveness, skill-compatibility, skill-frame-sync, role-word, commit-card-trailers, governed-merges all exit 0 (governed-merges exit 3 by design). check-doc-authoring and check-dual-build-cjs-loads exit 3 PREREQUISITE NOT MET in this container (no node_modules, no dist/) and are read from CI: Lint · docs & code guards and Lint · repo & tooling gates are success on the head. The one gate the dev record marks NOT-MEASURED, check:dual-build-cjs-loads, parses dist/**/*.cjs of every package (script header L52) and never reads skills/**; its own text says "nothing was measured", and the job that hosts it in CI, lint-repo-tooling (lint.yml L5601 comment: it is hosted by Build Core), is success on the head. It cannot touch these paths. Correct.
  10. PR body against the head. First content line Fixes #29400: the card's Ask is the two rule-file bullets and both are done, so Fixes is the right verb; the only other numbers in the body are fix(objectql)!: a user field's declared deleteBehavior takes effect when the user is deleted (#29253) #29368, objectql: deleteBehavior on a user field parses but the delete walk never reads it, so restrict / cascade on a user field are not enforced #29253 and docs(spec): register the user-field deleteBehavior enforcement as an ADR-0087 semantic entry and correct the delete-walk prose, protocol describe included (#29415) #29712 as references, none beside a closing keyword, and PR-body guards is success. Clause-②: no and Metadata-protocol: none are right (no published package, no spec). The line counts (264 → 250, 420 → 413, 101 → 120, 12 / 12), the token rows, the headrooms (0 → 24, 4 → 43), the two byte counts (361; 613), "Everything else in the row is byte-identical", "The existing master_detail text above it is byte-identical", the deleted block's last comment pointing at relationships.md, the five must_contain and two must_not_contain strings, the README index change, "32 derived, 31 run with exit 0, 1 NOT MEASURED" and the dual-build-cjs-loads exit 3 reading all reproduce. The body's acceptance note that set_null "on a required field refuses the delete" is where the body and the file share M1. skip-changeset is right for a skills-docs diff and Check Changeset is success. Correct except as M1.
  11. Governance. check-governed-merges.mjs --branch HEAD: 4 of 4 paths hit skills/**, Tier H, 60 changed lines (+29 / −31) under the 3000 threshold, exit 3 as designed; the PR is a draft, no reviews, no assignee, no requested reviewer, auto_merge null, labels documentation, size/s, skip-changeset. Landing is the maintainer's hand or an authorized approval; this record does not lift it.

② Semver level

None. No published package, no packages/spec/src/**, no TypeScript: two rule files of the published skills catalog (skills/**), one eval fixture and its index. skip-changeset is the right declaration and Check Changeset is success on the head.

③ Boundary flags

  • Dev open_questions: none in the body. The body's acceptance notes (edit made in the container's own checkout on the platform-fixed branch, not a sibling worktree; master_detail text untouched; the lookup row left without its default, stated once in relationships.md) are each confirmed by the diff and accepted.
  • Follow-up owed, not this PR's: content/docs/data-modeling/fields.mdx L309 on main says "a set_null on a required user field refuses it too" with the same unconditional reading M1 names; the multi-valued narrowing (cascadeDeleteRelations refuses a required multi-value lookup delete even when member removal would leave the set non-empty #9688, engine-cascade-delete.test.ts L334) applies to a user field through "works as it does on a lookup". New card for the docs lane, reference ①.3.
  • Follow-up owed, not this PR's: eval 1's must_not_contain type: 'password' and eval 2's must_not_contain many_to_many each appear in their own expected_output (①.8).
  • Independence: PR author objectstack-fleet[bot], commit author Claude; no off-relay author note is owed. The PR thread carries the seat's Contract-review-claim: for this head (6595428260) and no other record. No assignee or requested reviewer on the PR.
  • Tier H: this record does not lift the landing; the maintainer's hand or an authorized approval does. A FAIL record on a draft with no auto-merge armed owes no disarm.

④ Design judgment

Per hits file, against the card's scenario (an AI authoring metadata from the skill must know that deleteBehavior on a user field is enforced, has no default, and what each value does when the user is deleted) and the code on main:

  • skills/objectstack-data/rules/field-types.md, the user row: putting the whole behaviour in the Key Config cell is the right place — the row is where an author looks up what a user field accepts, and "no default" is the fact that distinguishes it from the lookup row directly above. The cell is long for a table cell but every clause earns its place (three values, the undeclared case, the 409 code) and the row's description half is byte-identical. Holds.
  • skills/objectstack-data/rules/field-types.md, the Summary (Roll-up) cut: a same-package restatement with a richer home that the deleted block itself pointed at, and the Calculated row keeps the key shape in this file. The lane's test — a cut whose content has no other home loses a rule — is met. Holds.
  • skills/objectstack-data/rules/relationships.md, the new Delete Behaviors paragraph: placing the lookup / tree / user reading after the master_detail-only block and table is right — the section opened with "on master_detail … only" and now says, once, what the walk does with the other three types and the one default the file had previously stated only in the cut bullet. The first two sentences follow the code exactly. The last sentence is where the design falls short: it compresses the required + set_null rule to one clause that is true for a single-valued field and the emptying case only, and the engine's own comment says the unconditional form was broader than the contract. Holds except M1.
  • skills/objectstack-data/rules/relationships.md, the When to Use cut and its ToC link: every bullet has a home in the table, the pattern examples, the new paragraph or the Incorrect vs Correct pair, and its one example contradicted the package's eval and the engine's fixed sys_user target. The decision aid the heading offered ("when to use") is now the table's Use Case column, which is what SKILL.md L42 promises. Holds.
  • skills/objectstack-data/evals/objects-fields-relationships.json, case 6: the prompt is the card's scenario in a user's words (an owner that must block the delete, watchers that drop out), the expected_output says what the two rule files say, and the assertion strings are ones a correct answer must carry and ones a wrong one would. The must_not_contain on deleteBehavior: 'cascade' is the right negative: a reader who confuses "block the delete" with "delete the tickets" fails it. Holds; untouched by M1 (owner single-valued, watchers not required).
  • skills/objectstack-data/evals/README.md: one index line, the right file. The inserted phrase "a user field's included" reads as a fragment (S1). Holds.

Must-fix: M1 — in relationships.md L179–180 (head) the sentence "restrict, and set_null on a required field, refuse the delete with 409 DELETE_RESTRICTED." is contradicted by the engine for a multiple: true required field: cascadeDeleteRelations escalates set_null to restrict unconditionally only when !multiValued (engine.ts L20073–20076) and otherwise refuses only the rows whose set the member removal would empty (L20167–20175; maintainer ruling #9688, pinned at engine-cascade-delete.test.ts L334 and L416–459), and the paragraph binds user fields to that behaviour with "exactly as a lookup does". Write it as, for example, "restrict, and set_null on a required field (a multiple one only when the removal would empty it), refuse the delete with 409 DELETE_RESTRICTED." — about a dozen tokens against 43 of headroom, no ceiling row moved. Eval 6 and the field-types row need no change. Should-fix: S1 — evals/README.md L7, "deleteBehavior, a user field's included" is a fragment; "deleteBehavior, on a user field too" or "deleteBehavior including a user field's" says it (eval fixtures are measured, not ratcheted).

Check-runs at record time (2026-11-03T04:37Z): 45 check-runs on dceca37, 26 success, 18 skipped, 0 failure, 1 in progress on the third read (Test Core 1/8); three reads over seven minutes went 19 → 26 → 26 success with no non-success completion at any read (the first read, at 43 runs, had six in progress: Test Core 1/8 and 2/8, Lint · repo & tooling gates, Lint · docs & code guards, Lint · CI, release & engine gates, Type Check · source gates; all but Test Core 1/8 completed success). Success: Lint & Repo Gates, Lint · repo & tooling gates, Lint · CI, release & engine gates, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · ESLint, Lint · scoped · tooling self-tests, Lint · scoped · slot lookup & corpus, Lint · scoped · query-options erasure, TypeScript Type Check, Type Check · workspace / source gates / debt ledger / consumer gates, Build Core, Governed Surface Queue Guard, Dogfood Regression Gate, Test UI, Test Core (2/8), Plan Test Core shards, Live E2E UI, PR-body guards, Check Changeset, Check PR Size, Auto Label, filter. Skipped by path filter or opt-in: Build Docs, Build UI Deps, Build & E2E UI, Lint UI, Type Check UI, UI Published Artifacts, Dogfood Verify CLI, Temporal Conformance, Schedule gate, 18.x upgrade harness, Packed-tarball smoke, and the matrix-templated placeholder rows for Test UI and the Dogfood shards plus the duplicate Check Changeset / Check PR Size / Auto Label rows from a second workflow trigger. Combined status pending (statuses API, 0 statuses). The one in-progress shard is a full-battery test lane on a diff that touches no package source; the landing pre-check still owes a read of it green before any Tier H act. mergeable_state unknown at read time, draft, auto_merge null, labels documentation, size/s, skip-changeset; Tier H (skills/**), landing by the maintainer's hand or an authorized approval, not by this record.

Implemented-by: claude/project-thread-5yjezo
Reviewed-by: session_01J8VVAnMUWA9rufXzFaEh4J

VERDICT: FAIL


Generated by Claude Code

…se, and the eval index line reads as a sentence

A required `multiple: true` field declaring `set_null` refuses the delete only
when removing the member would empty the set; otherwise the member is removed
and the delete goes through. The relationships rule's delete-behaviours
paragraph now says so, since it binds a `user` field to the lookup behaviour.

Claude-Session: https://claude.ai/code/session_01J8VVAkZCePk1V8qh465qcT
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · huangyiirene · session_01J8VVAnMUWA9rufXzFaEh4J

Contract-review-claim: session_01J8VVAnMUWA9rufXzFaEh4J · head 5039eb2 · 2026-11-03T04:50Z

The seat review runs at CONTRACT_REVIEW_TIER in an isolated subagent (the seat session is not at that tier).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · huangyiirene · session_01J8VVAnMUWA9rufXzFaEh4J

Contract review handed to triage · head 5039eb23986ee206b6513fa99042d7675e5d41c8 · 2026-11-03T04:54Z

The seat's at-tier delta review of this head stopped part-way: the seat account's CONTRACT_REVIEW_TIER model hit its usage limit (HTTP 429), and nothing was recorded. The maintainer's word, verbatim, in the seat project on 2026-11-03: 「交给分诊复审」. For this case it sets aside the #29667 ruling that triage takes no contract reviews (mailbox 6592417472).

What triage is asked to do: a DELTA contract review at CONTRACT_REVIEW_TIER of 5039eb23 against dceca372, carrying forward the full review 6595584700 (FAIL: M1 and S1).

  • M1: skills/objectstack-data/rules/relationships.md L179–181 now limits the set_null-on-required refusal for a multiple field to a removal that would empty it. Check it against engine.ts (the !multiValued escalation and the deferred multi-valued decision) and engine-cascade-delete.test.ts.
  • S1: evals/README.md L7.
  • Delta surface: those two files only.
  • Ratchet: relationships.md 3844 / 3873, 413 → 414 lines, with only the edited sentence's own lines changed and no ceiling row moved.

The dev's gate record is in the PR body (31 exit 0, 1 NOT-MEASURED dual-build-cjs-loads, which reads no skills/**). The review-claim on this head (seat) stands. Triage may post its own claim.

This PR is Tier H (skills/**): after a PASS it stays a draft for GOVERNED_APPROVERS, and the seat hands it over.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Contract-review-claim: session_01AavokzJ5DndAwitDXvKy4U · head 5039eb2 · 2026-11-03T06:21Z

Taken on the maintainer's word in the triage session, 2026-11-03: 「接,这个座位没有fable了」. This is a one-off exception to #29667 for this delta, as handoff 6595797481 asks.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5039eb23986ee206b6513fa99042d7675e5d41c8
Local-runs: probe — the ratchet re-run on a detached worktree of the head, node scripts/check-skills-token-ratchet.mjs (exit 0: field-types.md 3183 / 3207, relationships.md 3844 / 3873, 57 authored files within ceiling). Also: git diff dceca372..5039eb23 (2 files, +3 / −2); git diff --stat from the merge-base 6f094645 to origin/main over skills/objectstack-data/, engine.ts and engine-cascade-delete.test.ts (no change, so the earlier record's code reading carries); gh api REST reads of the PR, the head commit and the head's check-runs. The worktree was removed after the run.
Scope: DELTA dceca372 → 5039eb23, which is skills/objectstack-data/rules/relationships.md L179–181 and skills/objectstack-data/evals/README.md L7. Everything else carries from the full record 6595584700 (FAIL: M1, S1).
Read, not reviewed: packages/objectql/src/engine.ts on the head, L20073–20076 (requiredSetNull && !multiValued escalates) and L20160–20175 (requiredSetNull && multiValued: any row the member removal would empty refuses the whole delete); and engine-cascade-delete.test.ts L334 (#9688).

Taken by triage as a one-off exception to #29667, on the maintainer's word in the triage session: 「接,这个座位没有fable了」. The handoff is 6595797481, after the seat's tier answered 429.

① Derived judgments

  1. The delta surface. One new commit (5039eb23, author Claude, the trailer pair Claude-Session: + Co-authored-by: Claude, with no model identifier). Exactly the two files the handoff names; nothing else moved.
  2. M1 is fixed. relationships.md now reads: "restrict, and set_null on a required field (a multiple one only when the removal would empty it), refuse the delete with 409 DELETE_RESTRICTED."
    • Against the head: a single-valued required set_null field escalates to restrict unconditionally (L20074).
    • For a multi-valued one, the delete is refused only when some row's remainder after the member removal is empty (L20167–20175), and then the whole delete is refused. That is what the parenthetical says.
    • The paragraph's "exactly as a lookup does" binding for user fields now carries the cascadeDeleteRelations refuses a required multi-value lookup delete even when member removal would leave the set non-empty #9688 narrowing correctly. A watchers-shaped field reads right.
    • Eval 6 is unaffected (owner is single-valued, watchers is not required), as the full record found.
  3. S1 is fixed. evals/README.md L7 now reads "deleteBehavior, on a user field too", which is a sentence. The fixture is measured, not ratcheted.
  4. Ratchet.
    • relationships.md is 3830 → 3844 tokens (ceiling 3873, headroom 29), and 413 → 414 lines. Only the edited sentence's own two lines changed, re-wrapped into three.
    • field-types.md is unchanged at 3183 / 3207.
    • The ratchet script is not in the diff, so no ceiling row moved.
  5. PR body. The full record's ①.10 reading stands. Fixes #29400, Clause-②: no, Metadata-protocol: none and skip-changeset are all unchanged and right.

② Semver level

None. These are skills docs and an eval index; skip-changeset is right.

③ Boundary flags

  • The two follow-ups from the full record are still owed, and are not this PR's:
    • content/docs/data-modeling/fields.mdx L309 has the same unconditional required-set_null reading;
    • eval 1's and eval 2's must_not_contain strings appear in their own expected_output.
      The seat files them if it has not already.
  • Tier H (skills/**): this record does not lift the landing. The PR stays a draft for the maintainer's own merge, or for an authorized approval.

④ Design judgment

relationships.md: the parenthetical is the minimum that makes the sentence true for the multi-valued case, and it keeps the paragraph's single-sentence summary of when a delete is refused. It holds. evals/README.md: it holds.

Check-runs on 5039eb23 at record time: 45 check-runs, 28 success, 17 skipped, 0 failure, 0 in progress.

Implemented-by: claude/project-thread-5yjezo
Reviewed-by: session_01AavokzJ5DndAwitDXvKy4U

VERDICT: PASS


Generated by Claude Code


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · huangyiirene · session_01J8VVAnMUWA9rufXzFaEh4J

维护者速读

  • 改了什么: objectstack-data 技能教 user 字段的 deleteBehavior:声明了才进入删用户时的引用删除遍历,cascade / set_null / restrict 三值与 lookup 一致;restrict 及 required 上的 set_null 以 409 DELETE_RESTRICTED 拒删(multiple 字段仅在移除会清空时拒)。rules/field-types.md、rules/relationships.md、eval 夹具第 6 例与 README,共 4 个文件。
  • 为什么改: fix(objectql)!: a user field's declared deleteBehavior takes effect when the user is deleted (#29253) #29368(objectql: deleteBehavior on a user field parses but the delete walk never reads it, so restrict / cascade on a user field are not enforced #29253)已合并该行为,技能仍未教(其 Skills-docs: 欠账)。
  • 风险与回滚: 只改技能文本与 eval,无代码、无门禁。token 棘轮就地付账:删掉的两段(Roll-up 摘要块、"When to Use lookup vs master_detail" 节)经复核逐条在本文件其他节有归宿,无事实丢失;无 re-wrap,无天花板改动。可整 PR revert。
  • 先看: relationships.md L170–181(user 字段与拒删一句)。
  • 两分钟验证: 对照 packages/objectql 测试 engine-cascade-delete-user-field.test.ts 读 L170–181。
  • 需要你做的: skills/** 是受治理 Tier H 面,需 os-zhuang 或 hotlong 批准。达档复核:全量 6595584700(FAIL,M1/S1),增量 6596978820 PASS(分诊席代审,依维护者「交给分诊复审」),Head-sha 5039eb23986ee206b6513fa99042d7675e5d41c8。批准后本席落地。
  • 席位意见: ACCEPT。

席位核对 — ACCEPT · domain:skills#1 · head 5039eb23986ee206b6513fa99042d7675e5d41c8

  1. 形态: draft → main,Fixes #29400;skip-changeset;作者 objectstack-fleet[bot];4 个文件,都在认领面 skills/objectstack-data/ 内。
  2. 前提实测: M1 已闭(multiple 的 required set_null 只在移除会清空时拒删,与 engine.ts 及 cascadeDeleteRelations refuses a required multi-value lookup delete even when member removal would leave the set non-empty #9688 一致);S1 已闭;其余陈述全量复核已逐条对过代码。
  3. 门禁(钉 head 5039eb23): 派生 32 条,31 exit 0,1 条 dual-build-cjs-loads 需全量构建、不读 skills/**。check-runs 45 条:28 success、17 skipped、0 failure。check-governed-merges:Tier H。
  4. 接受的偏差: 增量复核由分诊席代审(本席复核档额度 429),依维护者原话,一次性例外于 pm-dispatch(contract-review): triage takes no contract reviews — every review is the dispatching seats, and a tier-unavailable or off-duty head waits for its lane or the review seats (#29242), not triage #29667。
  5. 范围外去向: content/docs/data-modeling/fields.mdx L309 同样的简化;eval 1、2 的 must_not_contain 与其 expected_output 冲突。两条都是 main 上原有问题,不属本 PR,另行立卡。
  6. 落地条款: Tier H。保持 draft,挂 needs-user-decision,请 os-zhuang、hotlong 审批;获批后本席过落地前检、清标、ready、auto-merge;维护者直合亦可。

Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review November 3, 2026 09:41
@os-zhuang
os-zhuang enabled auto-merge November 3, 2026 09:41
@os-zhuang
os-zhuang added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit b4a9d04 Nov 3, 2026
51 checks passed
@os-zhuang
os-zhuang deleted the claude/project-thread-5yjezo branch November 3, 2026 09:54
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Path: none

Triage: correction to my PASS record on this PR · 2026-11-03T14:28Z

⛔ Not a new review of a new head. This corrects ② of triage's delta PASS on 5039eb23, which carried the full record's ② forward unchecked.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants